﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using EzDeploy.Resources;
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace EzDeploy.Core
{
	public class EmbeddedAssemblyILWriter : IEmbeddedAssemblyILWriter
	{
		public Result Write(Project project, IEnumerable<DeployGridItem> items)
		{
			string outputFile = Path.Combine(project.DeployedDirectory, Path.GetFileName(project.AssemblyFile));
			var assembly = AssemblyDefinition.ReadAssembly(project.AssemblyFile, new ReaderParameters(ReadingMode.Deferred));

			foreach (var type in assembly.MainModule.Types)
			{
				if (String.Equals(type.Name, "<Module>", StringComparison.InvariantCulture))
				{
					var resourceMap = this.InjectAssemblyResourceMapField(assembly.MainModule, type);
					var assemblyResolve = this.InjectAssemblyResolveMethod(assembly.MainModule, type, resourceMap);
					this.InjectInitializerMethod(items, assembly.MainModule, type, resourceMap, assemblyResolve);

					break;
				}
			}

			try
			{
				if (assembly.MainModule.Assembly.Name.HasPublicKey)
				{
					if (!string.IsNullOrEmpty(project.StrongnameKeyFile))
					{
						if (String.Equals(Path.GetExtension(project.StrongnameKeyFile), ".snk", StringComparison.InvariantCultureIgnoreCase))
						assembly.Write(outputFile, new WriterParameters { StrongNameKeyPair = new System.Reflection.StrongNameKeyPair(File.ReadAllBytes(project.StrongnameKeyFile)) });
						else
							return new Result(ResultType.Error, null, string.Format(MessageResource.NotSupportedStrongKeyNameFileFmt, Path.GetFileName(project.StrongnameKeyFile)));
					}
					else
						assembly.Write(outputFile);
				}
				else
					assembly.Write(outputFile);

				return new Result(ResultType.Success, string.Format(MessageResource.EmbeddedOperationSuccessSummaryFmt, Path.GetFileName(project.AssemblyFile), items.Count().ToString()));
			}
			catch (Exception ex)
			{
				return new Result(ResultType.Error, string.Format(MessageResource.EmbeddedOperationFailedFmt, ex.Message, Environment.NewLine));
			}
		}

		private MethodDefinition InjectInitializerMethod(IEnumerable<DeployGridItem> items, ModuleDefinition module, TypeDefinition type, FieldDefinition resourceMap,  MethodDefinition assemblyResolveMethod)
		{
			var method = new MethodDefinition(".cctor", MethodAttributes.SpecialName | MethodAttributes.Static | MethodAttributes.RTSpecialName, module.TypeSystem.Void);
			var ip = method.Body.GetILProcessor();

			var appDomain_GetCurrentDomain = module.Import(typeof(AppDomain).GetProperty("CurrentDomain").GetGetMethod());
			var resolveEventHandler_Ctor = module.Import(typeof(ResolveEventHandler).GetConstructor(new Type[] { typeof(object), typeof(IntPtr) }));
			var resolveEventHandler_Add = module.Import(typeof(AppDomain).GetEvent("AssemblyResolve").AddMethod);

			var maps = new Dictionary<string, string>();
			this.EmbedAssembliesToMainAssembly(items, module, maps);
			this.InjectAssemblyResourceMap(module, ip, resourceMap, maps);

			ip.Emit(OpCodes.Call, appDomain_GetCurrentDomain);
			ip.Emit(OpCodes.Ldnull);
			ip.Emit(OpCodes.Ldftn, assemblyResolveMethod);
			ip.Emit(OpCodes.Newobj, resolveEventHandler_Ctor);
			ip.Emit(OpCodes.Callvirt, resolveEventHandler_Add);

			ip.Emit(OpCodes.Ret);

			type.Methods.Add(method);
			return method;
		}

		private FieldDefinition InjectAssemblyResourceMapField(ModuleDefinition module, TypeDefinition type)
		{
			var field = new FieldDefinition("_assemblyResourceMap", FieldAttributes.Static, module.Import(typeof(Dictionary<string, string>)));
			type.Fields.Add(field);

			return field;
		}

		private void InjectAssemblyResourceMap(ModuleDefinition module, ILProcessor ip, FieldDefinition assemblyResourceMapField, Dictionary<string, string> maps)
		{
			ip.Emit(OpCodes.Newobj, module.Import(typeof(Dictionary<string, string>).GetConstructor(Type.EmptyTypes)));
			ip.Emit(OpCodes.Stsfld, assemblyResourceMapField);

			var dictionary_Add = module.Import(typeof(Dictionary<string, string>).GetMethod("Add", new Type[] { typeof(string), typeof(string) }));

			foreach (var map in maps)
			{
				ip.Emit(OpCodes.Ldsfld, assemblyResourceMapField);
				ip.Emit(OpCodes.Ldstr, map.Key);
				ip.Emit(OpCodes.Ldstr, map.Value);

				ip.Emit(OpCodes.Callvirt, dictionary_Add);
			}
		}

		private MethodDefinition InjectAssemblyResolveMethod(ModuleDefinition module, TypeDefinition type, FieldDefinition assemblyResourceMapField)
		{
			var resolveArgs_Name = module.Import( typeof(ResolveEventArgs).GetProperty("Name").GetGetMethod());
			var dictionary_Item = module.Import( typeof(Dictionary<string, string>).GetProperty("Item").GetMethod);
			var assembly_GetExecutingAssembly = module.Import(typeof(System.Reflection.Assembly).GetMethod("GetExecutingAssembly"));
			var assembly_GetManifestResourceStream = module.Import(typeof(System.Reflection.Assembly).GetMethod("GetManifestResourceStream", new Type[] { typeof(string) }));
			var stream_Length = module.Import(typeof(Stream).GetProperty("Length").GetMethod);
			var stream_Read = module.Import(typeof(Stream).GetMethod("Read", new Type[] { typeof(byte[]), typeof(Int32), typeof(Int32) }));
			var assembly_Load = module.Import(typeof(System.Reflection.Assembly).GetMethod("Load", new Type[] { typeof(byte[]) }));
			var disposable_Dispose = module.Import(typeof(IDisposable).GetMethod("Dispose"));

			var assemblyType = module.Import(typeof(System.Reflection.Assembly));
			var byteType = module.Import(typeof(byte));
			
			var method = new MethodDefinition("CurrentDomain_AssemblyResolve", MethodAttributes.Private | MethodAttributes.Static, assemblyType);
			method.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object));
			method.Parameters.Add(new ParameterDefinition(module.Import(typeof(ResolveEventArgs))));

			method.Body.Variables.Add(new VariableDefinition(module.TypeSystem.String));
			method.Body.Variables.Add(new VariableDefinition(module.Import(typeof(Stream))));
			method.Body.Variables.Add(new VariableDefinition(module.Import(typeof(byte[]))));
			method.Body.Variables.Add(new VariableDefinition(assemblyType));

			var ip = method.Body.GetILProcessor();
			ip.Emit(OpCodes.Ldsfld, assemblyResourceMapField);
			ip.Emit(OpCodes.Ldarg_1);
			ip.Emit(OpCodes.Callvirt, resolveArgs_Name);
			ip.Emit(OpCodes.Callvirt, dictionary_Item);
			ip.Emit(OpCodes.Stloc_0);
			ip.Emit(OpCodes.Call, assembly_GetExecutingAssembly);
			ip.Emit(OpCodes.Ldloc_0);
			ip.Emit(OpCodes.Callvirt, assembly_GetManifestResourceStream);
			ip.Emit(OpCodes.Stloc_1);

			// labels
			var IL_001d = ip.Create(OpCodes.Ldloc_1);
			var IL_0024 = ip.Create(OpCodes.Ldloc_1);
			var IL_0050 = ip.Create(OpCodes.Ldloc_3);
			var IL_0044 = ip.Create(OpCodes.Leave_S, IL_0050);
			var IL_0046 = ip.Create(OpCodes.Ldloc_1);
			var IL_004f = ip.Create(OpCodes.Endfinally);

			ip.InsertAfter(method.Body.Instructions.Last(), IL_001d);
			ip.Emit(OpCodes.Brtrue_S, IL_0024);

			ip.Emit(OpCodes.Ldnull);
			ip.Emit(OpCodes.Stloc_3);
			ip.Emit(OpCodes.Leave_S, IL_0050);

			ip.InsertAfter(method.Body.Instructions.Last(), IL_0024);
			ip.Emit(OpCodes.Callvirt, stream_Length);
			ip.Emit(OpCodes.Conv_Ovf_I);
			ip.Emit(OpCodes.Newarr, byteType);
			ip.Emit(OpCodes.Stloc_2);
			ip.Emit(OpCodes.Ldloc_1);
			ip.Emit(OpCodes.Ldloc_2);
			ip.Emit(OpCodes.Ldc_I4_0);
			ip.Emit(OpCodes.Ldloc_2);
			ip.Emit(OpCodes.Ldlen);
			ip.Emit(OpCodes.Conv_I4);
			ip.Emit(OpCodes.Callvirt, stream_Read);
			ip.Emit(OpCodes.Pop);
			ip.Emit(OpCodes.Ldloc_2);
			ip.Emit(OpCodes.Call, assembly_Load);
			ip.Emit(OpCodes.Stloc_3);
			ip.InsertAfter(method.Body.Instructions.Last(), IL_0044);

			ip.InsertAfter(method.Body.Instructions.Last(), IL_0046);
			ip.Emit(OpCodes.Brfalse_S, IL_004f);
			ip.Emit(OpCodes.Ldloc_1);
			ip.Emit(OpCodes.Callvirt, disposable_Dispose);
			ip.InsertAfter(method.Body.Instructions.Last(), IL_004f);

			ip.InsertAfter(method.Body.Instructions.Last(), IL_0050);
			ip.Emit(OpCodes.Ret);

			var finallyHandler = new ExceptionHandler(ExceptionHandlerType.Finally);

			int endTryIndex = method.Body.Instructions.IndexOf(IL_0044);
			int endHandlerIndex = method.Body.Instructions.IndexOf(IL_004f);

			// catch handler
			finallyHandler.TryStart = IL_001d;
			finallyHandler.TryEnd = method.Body.Instructions[endTryIndex+1];
			// finally handler
			finallyHandler.HandlerStart = IL_0046;
			finallyHandler.HandlerEnd = method.Body.Instructions[endHandlerIndex+1];

			method.Body.ExceptionHandlers.Add(finallyHandler);

			type.Methods.Add(method);

			/*** IL CODE FROM ILSPY ***/
			/*
			.locals init 
			(
				[0] string resourceName,
				[1] class [mscorlib]System.IO.Stream 'stream',
				[2] uint8[] assemblyRawBytes,
				[3] class [mscorlib]System.Reflection.Assembly CS$1$0000
			)

			IL_0000: ldsfld class [mscorlib]System.Collections.Generic.Dictionary`2<string, string> BindingRedirect.Resolve::_assemblyResourceMaps
			IL_0005: ldarg.1
			IL_0006: callvirt instance string [mscorlib]System.ResolveEventArgs::get_Name()
			IL_000b: callvirt instance !1 class [mscorlib]System.Collections.Generic.Dictionary`2<string, string>::get_Item(!0)
			IL_0010: stloc.0
			IL_0011: call class [mscorlib]System.Reflection.Assembly [mscorlib]System.Reflection.Assembly::GetExecutingAssembly()
			IL_0016: ldloc.0
			IL_0017: callvirt instance class [mscorlib]System.IO.Stream [mscorlib]System.Reflection.Assembly::GetManifestResourceStream(string)
			IL_001c: stloc.1
			.try
			{
				IL_001d: ldloc.1
				IL_001e: brtrue.s IL_0024

				IL_0020: ldnull
				IL_0021: stloc.3
				IL_0022: leave.s IL_0050

				IL_0024: ldloc.1
				IL_0025: callvirt instance int64 [mscorlib]System.IO.Stream::get_Length()
				IL_002a: conv.ovf.i
				IL_002b: newarr [mscorlib]System.Byte
				IL_0030: stloc.2
				IL_0031: ldloc.1
				IL_0032: ldloc.2
				IL_0033: ldc.i4.0
				IL_0034: ldloc.2
				IL_0035: ldlen
				IL_0036: conv.i4
				IL_0037: callvirt instance int32 [mscorlib]System.IO.Stream::Read(uint8[], int32, int32)
				IL_003c: pop
				IL_003d: ldloc.2
				IL_003e: call class [mscorlib]System.Reflection.Assembly [mscorlib]System.Reflection.Assembly::Load(uint8[])
				IL_0043: stloc.3
				IL_0044: leave.s IL_0050
			} // end .try
			finally
			{
				IL_0046: ldloc.1
				IL_0047: brfalse.s IL_004f

				IL_0049: ldloc.1
				IL_004a: callvirt instance void [mscorlib]System.IDisposable::Dispose()

				IL_004f: endfinally
			} // end handler

			IL_0050: ldloc.3
			IL_0051: ret
			*/
			return method;
		}

		private void EmbedAssembliesToMainAssembly(IEnumerable<DeployGridItem> items, ModuleDefinition module, Dictionary<string, string> maps)
		{
			foreach (var item in items)
			{
				if (item.DeploymentType == DeploymentType.Include)
				{
					if (File.Exists(item.ItemFullPath))
					{
						string map = Guid.NewGuid().ToString();
						string fullName = item.AssemblyName.FullName;
						maps.Add(fullName, map);

						module.Resources.Add(new EmbeddedResource(map, ManifestResourceAttributes.Private, File.ReadAllBytes(item.ItemFullPath)));
					}
				}
			}
		}
	}
}
/*** WARNING: DO NOT REMOVE THIS COMMENT  ***/
/*
* Injecting IL manualy is hard, no more, :D.
*/
/*** END WARNING  ***/
